% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
%   http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.

%%% @author CA Meijer
%%% @copyright 2012 CA Meijer
%%% @doc This module provides a function for establishing a session with the 
%%%      Nimbula REST API and issuing HTTP GETs, POSTs, PUTs and DELETEs.
%%%      There are convenience functions for authenticating a user and to get
%%%      the username of the logged-in user.
%%% @end

-module(nimbula).

-behaviour(gen_server).

%% Include files
-include("nimbula_records.hrl").

%% API
-export([authenticate/3,
		 connect/1,
		 connect/3,
		 close/1,
		 delete/2,
		 get/3,
 		 get_user/1,
		 post/3,
		 post/5,
		 put/3]).

%% gen_server callbacks
-export([init/1, 
		 handle_call/3, 
		 handle_cast/2, 
		 handle_info/2, 
		 terminate/2, 
		 code_change/3]).

-record(state, {uri, httpc_profile, username}).


%% External functions
authenticate(Pid, User, Password) when is_binary(User) andalso is_binary(Password) ->
	gen_server:call(Pid, {authenticate, User, Password});
authenticate(Pid, User, Password) when is_list(User) ->
	authenticate(Pid, list_to_binary(User), Password);
authenticate(Pid, User, Password) when is_list(Password) ->
	authenticate(Pid, User, list_to_binary(Password)).

connect(Uri) ->
	UriWithSlash = nimbula_utils:add_trailing_slash(Uri),
	gen_server:start(?MODULE, [UriWithSlash], []).

connect(Uri, User, Password) ->
	case connect(Uri) of
		{ok, Pid} ->
			case authenticate(Pid, User, Password) of
				ok ->
					{ok, Pid};
				NotOk ->
					NotOk
			end;
		NotOk ->
			NotOk
	end.

close(Pid) ->
	gen_server:call(Pid, close).

delete(Pid, Object) ->
	ObjectStr = to_string(Object),
	gen_server:call(Pid, {delete, ObjectStr, nimbula_config:get_accept_type()}, infinity).

get(Pid, Object, AcceptType) ->
	ObjectStr = to_string(Object),
	gen_server:call(Pid, {get, ObjectStr, AcceptType}, infinity).

get_user(Pid) ->
	gen_server:call(Pid, get_user).
	
post(Pid, Path, Body) ->
	gen_server:call(Pid, {post, Path, Body, nimbula_config:get_accept_type(), 
						  nimbula_config:get_content_type()}, infinity).

post(Pid, Url, Headers, ContentType, Body) ->
	gen_server:call(Pid, {post_absolute, Url, Headers, ContentType, Body}, infinity).

put(Pid, Path, Body) ->
	put(Pid, Path, Body, nimbula_config:get_accept_type()).

put(Pid, Path, Body, Accept) ->
	put(Pid, Path, Body, Accept, nimbula_config:get_content_type()).

put(Pid, Path, Body, Accept, ContentType) ->
	gen_server:call(Pid, {put, Path, Body, Accept, ContentType}, infinity).


%% Server functions

%% Function: init/1
%% Description: Initiates the server
%% Returns: {ok, State}          |
%%          {ok, State, Timeout} |
%%          ignore               |
%%          {stop, Reason}
init([Uri]) ->
	Profile = nimbula_httpc_profile_id_server:get_profile_id(),
	inets:start(httpc, [{profile, Profile}]),
	httpc:set_options([{cookies, enabled}], Profile),
    {ok, #state{uri=Uri, httpc_profile=Profile}, nimbula_config:get_timeout()}.


%% Function: handle_call/3
%% Description: Handling call messages
%% Returns: {reply, Reply, State}          |
%%          {reply, Reply, State, Timeout} |
%%          {noreply, State}               |
%%          {noreply, State, Timeout}      |
%%          {stop, Reason, Reply, State}   | (terminate/2 is called)
%%          {stop, Reason, State}            (terminate/2 is called)
handle_call({authenticate, User, Password}, _From, State) ->
 	AuthenticateRecord = #authenticate{user=User, password=Password},
 	Json = nimbula_json:record_to_json(?record_fields(authenticate), AuthenticateRecord),
	Url = State#state.uri ++ "authenticate/",
	{ok, Response} = httpc:request(post, {Url, [{"accept", nimbula_config:get_accept_type()}],
								   nimbula_config:get_content_type(), Json}, [],
								   [{body_format, binary}],	State#state.httpc_profile),
	ParsedResponse = parse_http_response(Response),
	case ParsedResponse of
		{ok, _} ->
			Reply = ok,
			NewState = State#state{username=User};
		Error ->
			Reply = Error,
			NewState = State#state{username=undefined}
	end,
	{reply, Reply, NewState, nimbula_config:get_timeout()};
handle_call(close, _From, State) ->
    {stop, normal, ok, State};
handle_call({delete, Path, Accept}, _From, State) ->
	Url = State#state.uri ++ Path,
	{ok, Response} = httpc:request(delete, {Url, [{"accept", Accept}]}, [], [{body_format, binary}], 
								   State#state.httpc_profile),
	Reply = parse_http_response(Response),
	{reply, Reply, State, nimbula_config:get_timeout()};
handle_call({get, Path, Accept}, _From, State) ->
	Url = State#state.uri ++ Path,
	{ok, Response} = httpc:request(get, {Url, [{"accept", Accept}]}, [], [{body_format, binary}], 
								   State#state.httpc_profile),
	Reply = parse_http_response(Response),
	{reply, Reply, State, nimbula_config:get_timeout()};
handle_call(get_user, _From, State) ->
	{reply, State#state.username, State, nimbula_config:get_timeout()};
handle_call({post_absolute, Url, Headers, ContentType, Body}, _From, State) ->
	{ok, Response} = httpc:request(post, {Url, Headers, ContentType, Body}, [], 
								   [{body_format, binary}],	State#state.httpc_profile),
	Reply = parse_http_response(Response),
	{reply, Reply, State, nimbula_config:get_timeout()};
handle_call({post, Path, Body, Accept, ContentType}, _From, State) ->
	Url = State#state.uri ++ Path,
	{ok, Response} = httpc:request(post, {Url, [{"accept", Accept}], ContentType, Body}, [], 
								   [{body_format, binary}],	State#state.httpc_profile),
	Reply = parse_http_response(Response),
	{reply, Reply, State, nimbula_config:get_timeout()};
handle_call({put, Path, Body, Accept, ContentType}, _From, State) ->
	Url = State#state.uri ++ Path,
	{ok, Response} = httpc:request(put, {Url, [{"accept", Accept}], ContentType, Body}, [], 
								   [{body_format, binary}],	State#state.httpc_profile),
	Reply = parse_http_response(Response),
	{reply, Reply, State, nimbula_config:get_timeout()}.

%% Function: handle_cast/2s
%% Description: Handling cast messages
%% Returns: {noreply, State}          |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
handle_cast(_Msg, State) ->
    {noreply, State}.

%% Function: handle_info/2
%% Description: Handling all non call/cast messages
%% Returns: {noreply, State}          |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
handle_info(timeout, State) ->
    {stop, normal, State}.

%% Function: terminate/2
%% Description: Shutdown the server
%% Returns: any (ignored by gen_server)
terminate(_Reason, State) ->
	Profile = State#state.httpc_profile,
	inets:stop(httpc, Profile),
	nimbula_httpc_profile_id_server:restore_profile_id(Profile),
    ok.

%% Func: code_change/3
%% Purpose: Convert process state when code is changed
%% Returns: {ok, NewState}
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%% Internal functions
parse_http_response({StatusLine, _Headers, Body}) ->
	{_HttpVersion, StatusCode, _Reason} = StatusLine,
	parse_http_response({StatusCode, Body});
parse_http_response({StatusCode, Body}) when StatusCode >= 200 andalso StatusCode =< 299 ->
	{ok, Body};
parse_http_response({400, _Body}) ->
	{error, 400, "Client error"};
parse_http_response({401, _Body}) ->
	{error, 401, "Unauthorized."};
parse_http_response({403, _Body}) ->
	{error, 403, "Forbidden."};
parse_http_response({404, _Body}) ->
	{error, 404, "Not found."};
parse_http_response({405, _Body}) ->
	{error, 405, "Method not allowed."};
parse_http_response({406, _Body}) ->
	{error, 406, "Not acceptable."};
parse_http_response({408, _Body}) ->
	{error, 408, "Request timeout."};
parse_http_response({409, _Body}) ->
	{error, 409, "Conflict."};
parse_http_response({410, _Body}) ->
	{error, 410, "Gone."};
parse_http_response({415, _Body}) ->
	{error, 415, "Unsupported media type."};
parse_http_response({500, Body}) ->
	{error, 500, Body};
parse_http_response({503, _Body}) ->
	{error, 503, "Service unavailable."};
parse_http_response({StatusCode, _Body}) ->
	{error, StatusCode, "Error. HTTP Response Code = " ++ integer_to_list(StatusCode) + "."}.

to_string(Value) when is_list(Value) ->
	Value;
to_string(Value) when is_binary(Value) ->
	unicode:characters_to_list(Value).
